package muddb;


import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class DatabaseManager {

  private static DatabaseManager instance;
  private static final String PERSISTENCE_META = "/META-INF/persistence.xml";
  private final DatabaseManager.PersistenceUnit builderPU;
  private final DatabaseManager.PersistenceUnit normalPU;
  private EntityManager entityManager;
  private EntityManagerFactory entityManagerFactory;

  public class PersistenceUnit {

    public File embeddedDerbyDbPath;
    public String driver;
    public String jdbcURI;
    public String name;
    public String password;
    public String user;
    private boolean embeddedDerby;
    private boolean driverAvailable;


    public PersistenceUnit( String name, String user, String password, String jdbcURI, String driver ) {
      this.name = name;
      this.user = user;
      this.password = password;
      this.jdbcURI = jdbcURI;
      this.driver = driver;

      embeddedDerby = driver.endsWith( "EmbeddedDriver" );

      if ( !embeddedDerby ) {
        embeddedDerbyDbPath = null;
      } else {
        File dbLoc = null;

        try {
          URI derbyURI = new URI( new URI(jdbcURI).getSchemeSpecificPart() );

          final StringBuilder dbName = new StringBuilder( 
            derbyURI.getSchemeSpecificPart() );
          final int pos = dbName.indexOf( ";" );
          if ( pos != -1 ) {
            dbName.setLength( pos );
          }
          System.out.println( "dbName: " + dbName );
          final String userDir = System.getProperty( "user.dir" );
          final String derbySysHome = System.getProperty( "derby.system.home",
                                                          userDir );

          dbLoc = new File( derbySysHome, dbName.toString() );
        //          System.out.println("dbLoc: " + dbLoc.toString() + ", exist: " + dbLoc.exists());
        } catch ( URISyntaxException ex ) {
          Logger.getLogger( DatabaseManager.class.getName() ).log( Level.SEVERE,
                                                                   null, ex );
        }
        embeddedDerbyDbPath = dbLoc;
      }

      // can we find the driver class?
      try {
        Class.forName(driver);
        driverAvailable = true;
      } catch (ClassNotFoundException ex) {
        // ignore exception;
      }
    }


    public boolean isDriverAvailable() {
      return driverAvailable;
    }

    public boolean isEmbeddedDerby() {
      return embeddedDerby;
    }
  }


  public DatabaseManager() throws java.io.IOException,
                                  javax.xml.parsers.ParserConfigurationException,
                                  org.xml.sax.SAXException,
                                  java.net.URISyntaxException {
    final DocumentBuilderFactory docBldrFac = DocumentBuilderFactory.newInstance();
    final DocumentBuilder docBldr = docBldrFac.newDocumentBuilder();
    final Document doc = docBldr.parse( getClass().getResourceAsStream( 
                                        PERSISTENCE_META ) );

    builderPU = findPersistenceUnit( doc, null );

    // if the persistenceUnit has been identified as "EmbeddedDerby",
    // we know how to check for it's existence.
//    if (builderPU != null && builderPU.isEmbeddedDerby() && !builderPU.embeddedDerbyDbPath.exists()) {
//      System.out.println("database does not exist; creating it.");
//
//      final EntityManagerFactory entityManagerFactory;
//      entityManagerFactory = Persistence.createEntityManagerFactory(builderPU.name);
//      final EntityManager em = entityManagerFactory.createEntityManager();
//      em.close();
//    }

    PersistenceUnit pu = findPersistenceUnit( doc, builderPU );
    if ( pu == null && builderPU != null ) {
      pu = builderPU;
    }
    normalPU = pu;
    System.out.println( "PersistenceUnit name: " + normalPU.name );
  }


  /**
   * Extracts a PersistenceUnit from the supplied document that matches the
   * criteria.
   * 
   * @param doc XML document which holds the contents of the
   * <code>persistence.xml</code> file.
   * 
   * @param builderPU If true, indicates a PersistenceUnit which is capable
   * of building the database from scratch is desired.
   * 
   * @return a PersistenceUnit extracted from <code>doc</code> which 
   * matches the criteria specified by <code>builderPU</code>.
   * 
   * @throws java.net.URISyntaxException If the JDBC URL within the
   * <code>persistence.xml</code> file is not of the expected format.
   */
  public DatabaseManager.PersistenceUnit findPersistenceUnit( Document doc, PersistenceUnit builderPU )
    throws java.net.URISyntaxException {
    XPathFactory xpathFac = XPathFactory.newInstance();
    XPath xpath = xpathFac.newXPath();

    // find the persistence-unit which :
    //  - has a JDBC URL for Derby
    //  - and matches our builderPU requirements
//    final StringBuilder predicate =
//      new StringBuilder( 
//      ".//property[@name='toplink.jdbc.url' and starts-with( @value, 'jdbc:derby:') ]" );
    final StringBuilder predicate = new StringBuilder(".//property[@name='toplink.jdbc.url' ]");

    predicate.append( " and " );

    if ( builderPU != null ) {

      // for "toplink.ddl-generation".
      predicate.append( "count(.//property[@name='toplink.ddl-generation'])=0" );
      predicate.append( " and " );
      predicate.append(".//property[@name='toplink.jdbc.url' and @value='");
      predicate.append( builderPU.jdbcURI );
      predicate.append("' ]");
    } else {
      // if we're looking for a builder PU, it will contain a property
      // for "toplink.ddl-generation".
      predicate.append( ".//property[@name='toplink.ddl-generation']" );
    //            predicate.append( " and ( @value='drop-and-create-tables' or @value='create-tables')]" );
    }

    PersistenceUnit pu = null;

    try {
      XPathExpression xpathExp = xpath.compile( "//persistence-unit[" + predicate + "]" );

      NodeList nodes = (NodeList) xpathExp.evaluate( doc, XPathConstants.NODESET );

      for ( int i = 0; i < nodes.getLength(); i++ ) {
        final Node node = nodes.item( i );

        final String jdbcUrlStr = xpath.evaluate( 
                                        "properties/property[@name='toplink.jdbc.url']/@value",
                                        node );
        
        pu =
          new PersistenceUnit( xpath.evaluate( "@name", node ),
                               xpath.evaluate( 
                               "properties/property[@name='toplink.jdbc.user']/@value",
                               node ),
                               xpath.evaluate( 
                               "properties/property[@name='toplink.jdbc.password']/@value",
                               node ),
                               jdbcUrlStr,
                               xpath.evaluate( 
                               "properties/property[@name='toplink.jdbc.driver']/@value",
                               node )  );

        //      System.out.println("user: " + pu.user + ", host: " + pu.jdbcURI.toString());
        if ( pu.isDriverAvailable() ) {
          break;
        }
      }
    } catch ( XPathExpressionException ex ) {
      Logger.getLogger( DatabaseManager.class.getName() ).log( Level.SEVERE,
                                                               null, ex );
    }

    return pu;
  }


  public boolean needsCreated() {
    return normalPU != null && normalPU.isEmbeddedDerby() && !normalPU.embeddedDerbyDbPath.exists();
  }


  public boolean canCreate() {
    return builderPU != null;
  }


  public void create() {
    // if the persistenceUnit has been identified as "EmbeddedDerby",
    // we know how to check for it's existence.
    if ( canCreate() && builderPU != null ) {
      System.out.println( "database does not exist; creating it." );

      final EntityManagerFactory emf;
      emf = Persistence.createEntityManagerFactory( builderPU.name );
      final EntityManager em = emf.createEntityManager();
      em.close();
      emf.close();
    }
  }


  public static List<File> findFiles( File dbFile ) {
    final List<File> foundFiles = new ArrayList<File>();

    if ( dbFile.isFile() ) {
      foundFiles.add( dbFile );
    } else {
      File[] dirContents = dbFile.listFiles();

      for ( File f : dirContents ) {
        if ( f.isFile() ) {
          foundFiles.add( f );
        } else {
          foundFiles.addAll( findFiles( f ) );
        }
      }
    }
    return foundFiles;
  }


  public void createArchiveFile( String archiveFileName, int absPathLength, List<File> files )
    throws java.io.IOException {
    ZipOutputStream zos = new ZipOutputStream( new FileOutputStream( 
                                               archiveFileName ) );

    for ( File f : files ) {
      ZipEntry zEntry = new ZipEntry( f.getAbsolutePath().substring( 
                                      absPathLength ) );
      zEntry.setSize( f.length() );
      zEntry.setTime( f.lastModified() );

      zos.putNextEntry( zEntry );
      InputStream is = new BufferedInputStream( new FileInputStream( f ) );
      int bytesRead;
      byte[] buffer = new byte[ 5120 ];

      while ( ( bytesRead = is.read( buffer ) ) != -1 ) {
        zos.write( buffer, 0, bytesRead );
      }
      zos.closeEntry();
    }
    zos.close();
  }


  public void archive() throws java.io.IOException {
    DateFormat dtFmt = new SimpleDateFormat( "yyyyMMddhhmmss" );

    // if the persistenceUnit has been identified as "EmbeddedDerby",
    // we know how to check for it's existence.
    if ( !needsCreated() ) {
      System.out.println( "database exists; archiving it." );

      final File dbDir = normalPU.embeddedDerbyDbPath;
      final File dbParentDir = dbDir.getParentFile();
      final int absPathLength = dbParentDir.getAbsolutePath().length() + 1;

      List<File> allFiles = findFiles( dbDir );

      StringBuilder sb = new StringBuilder( "archive_" );
      sb.append( dtFmt.format( Calendar.getInstance().getTime() ) );
      sb.append( ".zip" );

      createArchiveFile( sb.toString(), absPathLength, allFiles );
//      for (File f : allFiles) {
//        System.out.println(f.getAbsolutePath().substring(absPathLength));
//      }
    }
  }


  public Connection getConnection() throws SQLException{
    Connection con = null;
    
    if ( normalPU != null ){
      con = DriverManager.getConnection(normalPU.jdbcURI, normalPU.user, normalPU.password);
    }
    return con;
  }
  
  public EntityManager getEntityManager() {
    if ( entityManagerFactory == null ) {
      create();
      entityManagerFactory = Persistence.createEntityManagerFactory( 
        normalPU.name );
    }
    entityManager = entityManagerFactory.createEntityManager();

    return entityManager;
  }


  public static synchronized DatabaseManager getInstance() {
    if ( instance == null ) {
      try {

        instance = new DatabaseManager();
      } catch ( IOException ex ) {
        Logger.getLogger( DatabaseManager.class.getName() ).log( Level.SEVERE,
                                                                 null, ex );
      } catch ( ParserConfigurationException ex ) {
        Logger.getLogger( DatabaseManager.class.getName() ).log( Level.SEVERE,
                                                                 null, ex );
      } catch ( SAXException ex ) {
        Logger.getLogger( DatabaseManager.class.getName() ).log( Level.SEVERE,
                                                                 null, ex );
      } catch ( URISyntaxException ex ) {
        Logger.getLogger( DatabaseManager.class.getName() ).log( Level.SEVERE,
                                                                 null, ex );
      }
    }

    return instance;
  }
}

